package gugga.tasks.containers
{
	import flash.events.EventDispatcher;
	
	import gugga.collections.dictionary.Dictionary;
	import gugga.debug.Assertion;
	import gugga.eventHelpers.EventDescriptor;
	import gugga.eventHelpers.Listener;
	import gugga.tasks.IProgressiveTask;
	import gugga.tasks.ITask;
	import gugga.tasks.ProgressiveTaskDecorator;
	import gugga.tasks.events.ProgressiveTaskEvent;
	import gugga.tasks.events.TaskEvent;
	import gugga.tasks.events.TasksContainerEvent;

	[Event(name="start", type="TaskEvent")]
	[Event(name="interrupted", type="TaskEvent")]
	[Event(name="completed", type="TaskEvent")]
	[Event(name="progress", type="ProgressiveTaskEvent")]

	/**
	 * The <code>ProgressMonitor</code> is responsible for organizing and monitoring the 
	 * progress of a complex set of tasks. The <code>ProgressMonitor</code> also provides 
	 * a mechanism to set progress intervals for each task. If a progress interval is not 
	 * set for a task, then the <code>ProgressMonitor</code> calculates what part of the 
	 * progress interval to give for that task, giving an equal parts for each task added 
	 * to the <code>ProgressMonitor</code>.
	 * <p>
	 * With the use of the <code>ProgressMonitor</code> we can easily specify for a task 
	 * to reserve for example 30% of the progress interval, for another task to take 15% 
	 * of the progress interval, and for the rest to share what's left of the interval 
	 * (in the case these are 55%, which divided among 11 task result in 5% from the 
	 * progress interval for each task).
	 */
	public class ProgressMonitor extends EventDispatcher implements IProgressiveTask
	{
		private var mLastGeneratedItemID : Number = 0;
	
		private var mItemParts : Dictionary;
		private var mItems : Dictionary;
		private var mFixedItems : Dictionary;
		private var mCompletedItems : Dictionary;
		
		private var mProgressListeners : Dictionary;
		private var mCompleteListeners : Dictionary;
		
		private var mVariableItemsCount : Number = 0;
		private var mFixedItemsCount : Number = 0;
		private var mFixedItemsPartSum : Number = 0;
		
		private var mProgress : Number = 0;	
		private var mRunning : Boolean = false;
		
		public function ProgressMonitor() 
		{
			mItemParts = new Dictionary();
			mItems = new Dictionary();
			mFixedItems = new Dictionary();
			mCompletedItems = new Dictionary();
			mProgressListeners = new Dictionary();
			mCompleteListeners = new Dictionary();
		}
		
		/**
		 * Starts the <code>ProgressMonitor</code>.
		 */
		public function start() : void 
		{ 
			mRunning = true;
			dispatchEvent(new TaskEvent(TaskEvent.START));
			
			var completedItemsCount : Number = 0;
			for (var key : Object in mCompletedItems)
			{
				completedItemsCount++;
			}
			
			var itemsCount : Number = 0;
			for (key in mItems)
			{
				itemsCount++;
			}
			
			if(mCompletedItems.count == mItems.count)
			{
				//TODO: Schedule all these for next frame - same in Preconditions task
				//Someone can call start, but mRunning will be false, and event is still not dispatched!
				mRunning = false;
				
				dispatchEvent(new TaskEvent(TaskEvent.COMPLETED));
				//TODO: it was dispatchEventLater in previous version
			}
		}
		
		/**
		 * Adds the specified items to the <code>ProgressMonitor</code>. Each item will get 
		 * an equal part of the progress interval left from the interval not reserved for the 
		 * fixed-part items. The calculation algorith is the following:
		 * <p>
		 * <code>
		 * item_progress_part = (100 - fixed_part_items_parts) / (count(already_added_items) + count(new_items))
		 * </code>
		 * 
		 * @param Array of ITask or IProgressiveTask items
		 */
		public function addItems(aItems : Array) : void
		{
			for (var i : Number = 0; i < aItems.length; i++)
			{
				addTask(aItems[i]);
			}
		}
		
		/**
		 * Adds a single task to the <code>ProgressMonitor</code>. 
		 * 
		 * @param ITask the task to be added
		 */
		public function addTask(aTask : ITask) : void
		{
			if(aTask is IProgressiveTask)
			{
				addItem(IProgressiveTask(aTask));
			}
			else if(aTask is ITasksContainer)
			{
				monitorTasksContainer(ITasksContainer(aTask));
			}
			else if(aTask is ITask)
			{
				var progressiveDecorator : ProgressiveTaskDecorator
					= new ProgressiveTaskDecorator(aTask);	
			
				addItem(progressiveDecorator);	
			}
			else
			{
				Assertion.fail("Argument aTask is not instance of IProgressiveTask or ITask or ITasksContainer", this, arguments); 
			}
		}
		
		//TODO: removeTask()
		
		/**
		 * Adds a single task to the <code>ProgressMonitor</code>, which will reserve 
		 * a certain part of the progressing interval for itself. 
		 * 
		 * @param ITask the task to be added
		 * @param Number the percentage part reserved from the progress interval, solely 
		 * devoded to the specified task
		 */
		public function addFixedPartTask(aTask : ITask, aPercentagePart : Number) : void
		{
			if(aTask is IProgressiveTask)
			{
				addFixedPartItem(IProgressiveTask(aTask), aPercentagePart);
			}
			else if(aTask is ITask)
			{
				var progressiveDecorator : ProgressiveTaskDecorator
					= new ProgressiveTaskDecorator(aTask);
					
				addFixedPartItem(progressiveDecorator, aPercentagePart);	
			}
			else
			{
				Assertion.fail("Argument aTask is not instance of IProgressiveTask or ITask", this, arguments);
			}
		}
		
		/**
		 * Adds a prepared IProgressiveTask to the <code>ProgressMonitor</code>.
		 * 
		 * @param IProgressiveTask the item to be added
		 */
		public function addItem(aItem : IProgressiveTask) : void
		{
			mLastGeneratedItemID++;
			mVariableItemsCount++;
			
			mItems[mLastGeneratedItemID] = aItem;
			
			recalculateItemParts();
			setItemListeners(aItem, mLastGeneratedItemID);
		}
		
		/**
		 * Adds a prepared IProgressiveTask to the <code>ProgressMonitor</code>, which will reserve 
		 * a certain part of the progressing interval for itself. 
		 * 
		 * @param IProgressiveTask the item to be added
		 * @param Number the percentage part reserved from the progress interval, solely 
		 * devoded to the specified item
		 */
		public function addFixedPartItem(aItem : IProgressiveTask, aPercentagePart : Number) : void
		{
			mLastGeneratedItemID++;
			mFixedItemsCount++;
			
			mItems[mLastGeneratedItemID] = aItem;
			mItemParts[mLastGeneratedItemID] = aPercentagePart;
			
			mFixedItems[mLastGeneratedItemID] = aItem;
			mFixedItemsPartSum += aPercentagePart;
			
			recalculateItemParts();
			setItemListeners(aItem, mLastGeneratedItemID);
		}
		
		/**
		 * Removes all items added to the <code>ProgressMonitor</code>.
		 */
		public function removeAllItems() : void
		{
			for (var key : String in mItems)
			{
				removeItem(IProgressiveTask(mItems[key]));
			}
		}
		
		/**
		 * Removes the specified items from the <code>ProgressMonitor</code>.
		 * 
		 * @param Array the items to be removed
		 */
		public function removeItems(aItems : Array) : void
		{
			for (var i:Number = 0; i < aItems.length; i++)
			{
				removeItem(aItems[i]);
			}
		}
		
		/**
		 * Removes the specified item from the <code>ProgressMonitor</code>.
		 * 
		 * @param IProgressiveTask the item to be removed
		 */
		public function removeItem(aItem : IProgressiveTask) : void
		{
			var itemID : Number;
			
			for (var key:String in mItems)
			{
				if(mItems[key] === aItem)
				{
					itemID = Number(key);
					break;
				}
			}
			
			Assertion.warningIfNull(itemID, "Suplied item does not exists in the monitored collection", this, arguments);
			
			if(mFixedItems[itemID])
			{
				delete mFixedItems[itemID];
				mFixedItemsCount--;
				mFixedItemsPartSum -= mItemParts[itemID];
			}
			else
			{
				mVariableItemsCount--;
			}
			
			Listener(mProgressListeners[itemID]).stop();
			Listener(mCompleteListeners[itemID]).stop();
			
			delete mProgressListeners[itemID];
			delete mCompleteListeners[itemID];
			delete mCompletedItems[itemID];
			delete mItemParts[itemID];
			delete mItems[itemID];
			
			recalculateItemParts();
		}
		
		private function setItemListeners(aItem:IProgressiveTask, aItemID:Number) : void
		{
			mProgressListeners[aItemID] = Listener.createDataForwardingListener(
				new EventDescriptor(aItem, ProgressiveTaskEvent.PROGRESS),
				onItemProgressed,
				{itemID: aItemID}
			);
	
			mCompleteListeners[aItemID] = Listener.createSingleTimeDataForwardingListener(
				new EventDescriptor(aItem, TaskEvent.COMPLETED),
				onItemCompleted,
				{itemID: aItemID}
			);
		}
		
		private function recalculateItemParts() : void
		{
			var variableItemPart : Number = (100 - mFixedItemsPartSum) / mVariableItemsCount;
			
			for (var key : String in mItems)
			{
				if(!mFixedItems.containsKey(key))
				{
					mItemParts[key] = variableItemPart;
				}
			}
		}
	
		private function onItemProgressed(aEvent : Object, aData : Object = null) : void
		{
			if(mRunning)
			{
				var actualProgress : Number = 0;
				
				for (var key:String in mItems)
				{
					if(mCompletedItems.containsKey(key))
					{
						actualProgress += mItemParts[key];
					}
					else
					{
						actualProgress += mItemParts[key] * (IProgressiveTask(mItems[key]).getProgress() / 100);				
					}
				}
				
				if(actualProgress > mProgress)
				{
					mProgress = actualProgress;
					
					dispatchEvent(new ProgressiveTaskEvent(ProgressiveTaskEvent.PROGRESS, mProgress, 100, mProgress));
				}
			}
		}
		
		private function onItemCompleted(aEvent : Object, aData : Object = null) : void
		{
			var itemID : Number = aData.itemID;
			
			Listener(mProgressListeners[itemID]).stop();
			mCompletedItems[itemID] = true;
			
			if(mRunning)
			{	
				onItemProgressed(aEvent);
				
				if(mCompletedItems.count == mItems.count)
				{
					mRunning = false;
					
					dispatchEvent(new TaskEvent(TaskEvent.COMPLETED));
				}
			}
		} 
	
		public function isImmediatelyInterruptable() : Boolean 
		{
			return true;
		}
	
		public function interrupt() : void 
		{
			mRunning = false;
			dispatchEvent(new TaskEvent(TaskEvent.INTERRUPTED));
		}
		
		public function getProgress() : Number 
		{
			return mProgress;
		}
	
		public function isRunning() : Boolean 
		{
			return mRunning;
		}
		
		//TODO: stopMonitoringTasksContainer()
		
		public function monitorTasksContainer(aTasksContainer : ITasksContainer) : void
		{
			var containedTasks : Array = aTasksContainer.getAllTasks();
			addItems(containedTasks);
			
			aTasksContainer.addEventListener(TasksContainerEvent.TASK_ADDED, onMonitoredContainerTaskAdded);
			aTasksContainer.addEventListener(TasksContainerEvent.TASKS_ADDED, onMonitoredContainerTasksAdded);
			aTasksContainer.addEventListener(TasksContainerEvent.TASK_REMOVED, onMonitoredContainerTaskRemoved);
			aTasksContainer.addEventListener(TasksContainerEvent.TASKS_REMOVED, onMonitoredContainerTasksRemoved);
		}
			
		private function onMonitoredContainerTaskAdded(aEvent : TasksContainerEvent) : void 
		{
			var task : ITask = aEvent.Task;
			addTask(task);
		}
	
		private function onMonitoredContainerTasksAdded(aEvent : TasksContainerEvent) : void 
		{
			var tasks : Array = aEvent.Tasks;
			addItems(tasks);
		}
	
		private function onMonitoredContainerTaskRemoved(aEvent : TasksContainerEvent) : void 
		{
			var task : ITask = aEvent.Task;
			removeItem(IProgressiveTask(task));
		}
	
		private function onMonitoredContainerTasksRemoved(aEvent : TasksContainerEvent) : void 
		{
			var tasks : Array = aEvent.Tasks;
			removeItems(tasks);
		}
	}
}